從按下相機快門的那刻,到變成我們看見的圖片之前,發生了哪些事情?
在轉換程我們常見的 JPEG 格式前,相機前後基本上會經過以下步驟
光學部分(機身):包含鏡頭組、光圈、快門控制。
感光部分(感測器):包含感光元件、增益(即 ISO),接著進行類比與數位訊號的轉換。
訊號處理:包含去馬賽克 (demosaic)、銳化、白平衡與圖片壓縮等等。
一張照片的明暗,取決於感光元件接收到的總光量,這由攝影中最核心的三個元素共同決定,它們被稱為「曝光鐵三角」:
光圈 (aperture): 指鏡頭中控制光線進入「量」的孔洞大小。光圈越大(如 f/1.8),單位時間內進光越多,背景也越容易模糊(淺景深);光圈越小(如 f/16),進光越少,背景越清晰(深景深)。它就像我們眼睛的瞳孔。
快門速度 (shutter speed): 指感光元件暴露在光線下的「時間」。速度越快(如 1/1000秒),越能凝結瞬間的動作;速度越慢(如 1秒),則會記錄下光線的軌跡,形成動態模糊。它就像我們眨眼的速度。
感光度 (ISO): 指感光元件對光的「敏感度」。ISO 越高,對光線越敏感,可以在昏暗環境下拍照,但代價是會產生更多的數位雜訊(顆粒感)。它就像一個訊號放大器。
這三者相互制約,共同決定了最終的曝光量。但除了照片的整體明暗,相機性能還有一個至關重要的指標,那就是它能同時記錄多暗和多亮的細節能力。這就是「動態範圍」。
動態範圍 (dynamic range) 代表了相機的寬容度,當相機有高動態範圍時,能夠在照片保留的亮部與暗部細節就更多。通常來說,專業相機的動態範圍約有 12 到 14 級,這意味我們如果拍攝夕陽與地景等光比極大的狀況下,依然能夠很好的保留太陽與地面各自的細節。
前面提到在相機進行訊號處理時,會經過一個稱為「去馬賽克」的步驟。事實上,相機的感光元件每個單獨的像素點不會記錄完整的 RGB 資訊,通常都只會記錄其中一種顏色,稱為拜耳濾色鏡 (Bayer filter),如下圖所示:
(來源:Wikipedia)
我們可以從圖中看到,綠色的像素點是其他顏色的兩倍,這是因為人眼對綠色最敏感,所以這樣進行設計。
因為這種設計,相機的影像感測器必須透過相鄰的像素點「猜」出每個像素缺失的另外兩個顏色,這個過程就稱為去馬賽克。
在相機與部分手機可拍攝的 RAW 檔,即沒有經過去馬賽克這個步驟,保留了後製的最大彈性。與之相比,JPEG 檔就經過了我們前面提到的各種圖像處理步驟,產生了輕便但有損失資訊的檔案。RAW 通常是 12 或 14-bit 的檔案,代表一張照片能夠擁有 2^12 到 2^14 次方個亮度階級,而 JPEG 通常為 8-bit,這也是為什麼 JPEG 會相當大地丟失照片原本的資訊。
在拍攝照片前後,有一個方法來判斷我們的圖片是否過曝或欠曝,那就是直方圖 (histogram)。
直方圖是一個圖表,它統計了影像中每個亮度級別(0-255)的像素數量。
X 軸:像素的亮度值,從最暗 (0) 到最亮 (255)。
Y 軸:擁有該亮度值的像素總數。
如何解讀直方圖?
圖形偏左:大部分像素集中在暗部區域,代表這是一張欠曝的影像。
圖形偏右:大部分像素集中在亮部區域,代表這是一張過曝的影像。
圖形集中在中間:像素大多是中階灰,代表這是一張對比度較低、看起來灰濛濛的影像。
圖形分佈均勻,橫跨整個 X 軸:代表影像擁有從純黑到純白的完整色調範圍,通常對比度良好。
有了直方圖作為分析工具,我們就可以開始動手調整影像了。色調曲線 (tone curve) 是調整影像明暗關係最強大的工具之一。它本質上是一個映射函式,定義了「輸入」的亮度值應該被轉換成哪個「輸出」的亮度值。
S 型色調曲線 (S-Curve) 是最經典的一種,它的形狀就像字母 "S",而效果如下
壓暗畫面中較暗的部分。
提亮畫面中較亮的部分。
拉伸中間調的範圍。
這三者結合起來的效果,就是顯著地增強影像的對比度,讓畫面有電影感。
先安裝 Matplotlib
pip install matplotlib
接著在編輯器輸入以下內容
import cv2
import numpy as np
from matplotlib import pyplot as plt
def plot_histogram(image, title):
"""繪製彩色影像的 BGR 三通道直方圖"""
color = ('b', 'g', 'r')
# plt.figure() 創建一個新的圖形視窗
plt.figure()
plt.title(title)
plt.xlabel("Bins (Intensity)")
plt.ylabel("# of Pixels")
# 對 B, G, R 三個通道分別計算並繪製直方圖
for i, col in enumerate(color):
hist = cv2.calcHist([image], [i], None, [256], [0, 256])
plt.plot(hist, color=col)
plt.xlim([0, 256])
# plt.show() 會顯示所有創建的圖形,直到你手動關閉圖形視窗
def apply_s_curve(image):
"""應用一條 S 型曲線到影像上以增強對比度"""
# 建立一個查詢表 (Look-Up Table, LUT)
# 查詢表是一個陣列,其索引是輸入的像素值(0-255),其值是輸出的像素值
lut = np.zeros(256, dtype=np.uint8)
center = 128
for i in range(256):
# 將輸入值從 0-255 映射到 -1 到 1
x = (i - center) / center # x is in [-1, 1]
# 這裡的 3.0 控制曲線的陡峭程度,可以調整看看效果
val = 1 / (1 + np.exp(-x * 3.0)) # Sigmoid function variant
# 將結果從 0-1 映射回 0-255
lut[i] = int(val * 255)
# 使用 cv2.LUT() 函式將查詢表應用到影像的所有通道
return cv2.LUT(image, lut)
# --- 主程式 ---
image_path = 'duck.jpg' # 換成你自己的圖片
original_image = cv2.imread(image_path)
# 1. 顯示原始影像並繪製其直方圖
cv2.imshow("Original Image", original_image)
plot_histogram(original_image, "Original Image Histogram")
# 2. 應用 S 型曲線
s_curved_image = apply_s_curve(original_image)
# 3. 顯示處理後影像並繪製其直方圖
cv2.imshow("S-Curved Image", s_curved_image)
plot_histogram(s_curved_image, "S-Curved Image Histogram")
# 4. 儲存處理後的圖片
output_path = 'duck_s_curved.jpg'
cv2.imwrite(output_path, s_curved_image)
print(f"處理後的圖片已儲存為: {output_path}")
print("觀察圖片與直方圖的變化。")
print("手動關閉直方圖視窗後,按任意鍵關閉視窗。")
# 顯示所有 matplotlib 繪製的圖形
plt.show()
cv2.waitKey(0)
cv2.destroyAllWindows()
原圖
調整後